/*
 * Copyright (c) 2012 Johann Hanne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdint.h>
#include <i86.h> /* For FP_SEG/FP_OFF */

#include <putchar.h> /* Must be provided externally (i.e. not part of clib) */

/*
 * The PUT_CHAR_* macros are used to limit the buffer which we are writing
 * to; it calls putchar directly when no buffer is provided
 *
 * pend must point to the the char *after* the last char which we are allowed
 * to write to; please note that the pointer gets incremented even after
 * the limit has been reached so that vsnprintf can correctly return
 * the number of bytes which would have been written even if the buffer
 * was not sufficient
 */

/* For use with "char **p" */
#define PUT_CHAR_PP(c, p, pend) \
    do { \
        if ((p) == NULL || *(p) == NULL) { \
            putchar(c); \
        } else { \
            if (*(p) < (pend) || (pend) == NULL) **(p) = (c); \
            (*(p))++; \
        } \
    } while (0)

/* For use with "char *p" */
#define PUT_CHAR_P(c, p, pend) \
    do { \
        if ((p) == NULL) { \
            putchar(c); \
        } else { \
            if ((p) < (pend) || (pend) == NULL) *(p) = (c); \
            (p)++; \
        } \
    } while (0)

/* Write the ASCII representation of an unsigned long integer */
static void put_uli(unsigned long int num, unsigned int base, int uc,
                    char fill_ch, int w, char **buf, char *bufend) {
    unsigned long int divider = 1;
    int n = 1;
    int digit;

    while (num / divider >= base) {
        divider *= base;
        n++;
    }

    /* Write leading '0's/' 's */
    w -= n;
    while ((w--) > 0) {
        PUT_CHAR_PP(fill_ch, buf, bufend);
    }

    while (divider != 0) {
        digit = num / divider;
        num %= divider;
        divider /= base;
        PUT_CHAR_PP(digit + (digit < 10 ? '0' : ((uc ? 'A' : 'a') - 10)), buf, bufend);
    }
}

/* Write the ASCII representation of a long integer */
static void put_li(long int num, char fill_ch, int w, char **buf, char *bufend) {
    if (num < 0) {
        num = -num;
        PUT_CHAR_PP('-', buf, bufend);
    }

    put_uli(num, 10, 0, fill_ch, w, buf, bufend);
}

/* Write the ASCII representation of an unsigned integer */
static void put_ui(unsigned int num, unsigned int base, int uc, char fill_ch,
                   int w, char **buf, char *bufend) {
    unsigned int divider = 1;
    int n = 1;
    int digit;

    while (num / divider >= base) {
        divider *= base;
        n++;
    }

    /* Write leading '0's/' 's */
    w -= n;
    while ((w--) > 0) {
        PUT_CHAR_PP(fill_ch, buf, bufend);
    }

    while (divider != 0) {
        digit = num / divider;
        num %= divider;
        divider /= base;
        PUT_CHAR_PP(digit + (digit < 10 ? '0' : ((uc ? 'A' : 'a') - 10)), buf, bufend);
    }
}

/* Write the ASCII representation of an integer */
static void put_i(int num, char fill_ch, int w, char **buf, char *bufend) {
    if (num < 0) {
        num =-num;
        PUT_CHAR_PP('-', buf, bufend);
    }

    put_ui(num, 10, 0, fill_ch, w, buf, bufend);
}

/* Convert a single ASCII encoded digit into its integer value (decimal only) */
static int a_to_d(char ch) {
    if (ch >= '0' && ch <= '9') {
        return ch - '0';
    }

    return -1;
}

/* Convert an ASCII encoded string into its integer value (decimal only) */
static int a_to_i(const char *p, int *idx) {
    int num = 0;
    int digit;

    while ((digit = a_to_d(p[*idx])) >= 0) {
        num = num * 10 + digit;
        (*idx)++;
    }

    return num;
}

static size_t my_strlen(const char *s) {
    size_t ret = 0;
    while (*s != '\0') {
        s++;
        ret++;
    }
    return ret;
}

static void put_str(char *str, int w, char **buf, char *bufend) {
    char ch;

    /* Write leading ' 's */
    w -= my_strlen(str);
    while ((w--) > 0) {
        PUT_CHAR_PP(' ', buf, bufend);
    }

    /* Output the buffer content */
    while ((ch = *(str++)) != '\0') {
        PUT_CHAR_PP(ch, buf, bufend);
    }
}

static void put_strn(char *str, size_t size, int w, char **buf, char *bufend) {
    /* Write leading ' 's */
    w -= size;
    while ((w--) > 0) {
        PUT_CHAR_PP(' ', buf, bufend);
    }

    /* Output the buffer content */
    while (size-- > 0) {
        PUT_CHAR_PP(*(str++), buf, bufend);
    }
}

static void put_segment_offset(void *p, int w, char **buf, char *bufend) {
    /* Write leading ' 's */
    w -= 11;
    while ((w--) > 0) {
        PUT_CHAR_PP(' ', buf, bufend);
    }

    PUT_CHAR_PP('0', buf, bufend);
    PUT_CHAR_PP('x', buf, bufend);
    put_ui(FP_SEG(p), 16, 1, '0', 4, buf, bufend);
    PUT_CHAR_PP(':', buf, bufend);
    put_ui(FP_OFF(p), 16, 1, '0', 4, buf, bufend);
}

int vsnprintf(char *str, size_t size, const char *format, va_list ap) {
    char *buf = str;
    char *bufend = (size > 0) ? (str + size) : NULL;
    int fmtidx = 0;
    char ch;
    char fill_ch = ' '; /* fill character */
    int w = 0; /* width */
    uint8_t is_long; /* format specifier with "long" flag */
    uint8_t is_size = 0; /* format specifier with "size_t/ssize_t" flag */

    while ((ch = format[fmtidx++])) {
        if (ch != '%') {
            PUT_CHAR_P(ch, buf, bufend);
        } else {
            fill_ch = ' '; /* fill character */
            w = 0; /* width */
            is_long = 0;
            is_size = 0;

            ch = format[fmtidx++];
            if (ch == '0') {
                fill_ch = '0';
                ch = format[fmtidx++];
            }
            if (ch >= '0' && ch <= '9') {
                fmtidx--;
                w = a_to_i(format, &fmtidx);
                ch = format[fmtidx++];
            }
            if (ch == 'l') {
                is_long = 1;
                ch = format[fmtidx++];
            } else if (ch == 'z') {
                is_size = 1;
                ch = format[fmtidx++];
            }

            switch (ch) {
            case '\0':
                /* Got string '\0' termination within format specifier! */
                goto stop;
            case 'u':
                if (is_long)
                    put_uli(va_arg(ap, unsigned long int), 10, 0, fill_ch, w, &buf, bufend);
                else if (is_size)
                    put_ui((unsigned int)va_arg(ap, size_t), 10, 0, fill_ch, w, &buf, bufend);
                else
                    put_ui(va_arg(ap, unsigned int), 10, 0, fill_ch, w, &buf, bufend);
                break;
            case 'd':
                if (is_long)
                    put_li(va_arg(ap, unsigned long int), fill_ch, w, &buf, bufend);
                else if (is_size)
                    put_i((unsigned int)va_arg(ap, size_t), fill_ch, w, &buf, bufend);
                else
                    put_i(va_arg(ap, int), fill_ch, w, &buf, bufend);
                break;
            case 'x':
            case 'X':
                if (is_long)
                    put_uli(va_arg(ap, unsigned long int), 16, (ch == 'X'), fill_ch, w, &buf, bufend);
                else if (is_size)
                    put_ui((unsigned int)va_arg(ap, size_t), 16, (ch == 'X'), fill_ch, w, &buf, bufend);
                else
                    put_ui(va_arg(ap, unsigned int), 16, (ch == 'X'), fill_ch, w, &buf, bufend);
                break;
            case 'o':
                if (is_long)
                    put_uli(va_arg(ap, unsigned long int), 8, 0, fill_ch, w, &buf, bufend);
                else if (is_size)
                    put_ui((unsigned int)va_arg(ap, size_t), 8, 0, fill_ch, w, &buf, bufend);
                else
                    put_ui(va_arg(ap, unsigned int), 8, 0, fill_ch, w, &buf, bufend);
                break;
            case 'c':
                PUT_CHAR_P((char)(va_arg(ap, int)), buf, bufend);
                break;
            case 's':
                put_str(va_arg(ap, char *), w, &buf, bufend);
                break;
            case 'p':
                put_segment_offset(va_arg(ap, void *), w, &buf, bufend);
                break;
            case '%':
                PUT_CHAR_P('%', buf, bufend);
                break;
            default:
                break;
            }
        }
    }

stop:

    PUT_CHAR_P('\0', buf, bufend);

    /* Return value must not include the terminating NUL byte */
    return (buf - str) - 1;
}

int vsprintf(char *str, const char *format, va_list ap) {
    return vsnprintf(str, 0, format, ap);
}

int snprintf(char *str, size_t size, const char *format, ...) {
    int ret;

    va_list ap;
    va_start(ap, format);
    ret = vsnprintf(str, size, format, ap);
    va_end(ap);

    return ret;
}

int sprintf(char *str, const char *format, ...) {
    int ret;

    va_list ap;
    va_start(ap, format);
    ret = vsnprintf(str, 0, format, ap);
    va_end(ap);

    return ret;
}

int printf(const char *format, ...) {
    int ret;

    va_list ap;
    va_start(ap, format);
    ret = vsnprintf(NULL, 0, format, ap);
    va_end(ap);

    return ret;
}

int vprintf(const char *format, va_list ap) {
    return vsnprintf(NULL, 0, format, ap);
}

int vfprintf(FILE *stream, const char *format, va_list ap) {
    if (stream == stdout || stream == stderr) {
        return vprintf(format, ap);
    }

    return -1;
}

int fprintf(FILE *stream, const char *format, ...) {
    int ret = -1;

    if (stream == stdout || stream == stderr) {
        va_list ap;
        va_start(ap, format);
        ret = vsnprintf(NULL, 0, format, ap);
        va_end(ap);
    }

    return ret;
}
